package com.aratana;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;

import com.aratana.annotation.Action;
import com.aratana.annotation.InjectFromModel;

public final class Controller {

	private final Logger logger;

	private final MVCUnit unit;
	private final HashMap<String, ActionInvoker> actions = new HashMap<String, ActionInvoker>();

	Controller(MVCUnit unit) {
		this.unit = unit;
		logger = Logger.getLogger("aratana." + getUnit().getName() + ".controller");
	}

	public MVCUnit getUnit() {
		return unit;
	}

	public void removeController(Object controller) {
		List<String> toRemove = new ArrayList<String>();

		for (String name : actions.keySet()) {
			if (actions.get(name).ref == controller) {
				toRemove.add(name);
			}
		}

		for (String name : toRemove) {
			actions.remove(name);
		}
	}

	public void addAsController(Object asController) {
		if (asController != null) {
			Class<?> clazz = asController.getClass();
			do {
				for (Method method : clazz.getDeclaredMethods()) {
					if (!Modifier.isStatic(method.getModifiers())) {

						String actionName = method.getName();

						Action action = method.getAnnotation(Action.class);
						if (action != null && !action.name().isEmpty()) {
							actionName = action.name();
						}

						if (unit.getView().existComponent(actionName)) {
							ActionInvoker invoker = actions.get(actionName);
							if (invoker == null) {
								invoker = new ActionInvoker(asController, method);
								actions.put(actionName, invoker);
							} else {
								logger.warn("Attemp to override a controller action ('" + actionName + "').");
							}
						}
					}
				}
				clazz = clazz.getSuperclass();
			} while (clazz != null);
		} else {
			logger.warn("Attemp to add a null controller.");
		}
	}

	public Object invoke(String name) {
		Object result = null;
		try {
			if (getUnit().getView().beforeAction(name)) {
				ActionInvoker invoker = actions.get(name);
				if (invoker != null) {
					logger.debug("Invoke " + invoker);
					result = invoker.invoke();
				}
			}
			getUnit().getView().afterAction(name);
		} catch (Exception e) {
			getUnit().handleException(e);
		}
		return result;
	}

	private final class ActionInvoker {

		private final Object ref;
		private final Method method;
		private final String name;
		private final InjectFromModel inject;

		ActionInvoker(Object ref, Method method) {
			this.name = method.getName();
			this.method = method;
			this.ref = ref;
			this.inject = method.getAnnotation(InjectFromModel.class);
			if ((inject == null || inject.value().length == 0) && method.getParameterTypes().length > 0) {
				logger.warn("Action method signature is invalid (" + name + "), only no-parameters is permited without @InjectFromModel.");
			}
		}

		private final Object invoke() throws Exception {
			final Class<?>[] parameters = method.getParameterTypes();
			if (parameters.length == 0) {
				return method.invoke(ref);
			} else if (inject != null && inject.value().length > 0) {
				ArrayList<Object> args = new ArrayList<Object>();

				if (parameters.length == 1) {
					if (Map.class.isAssignableFrom(parameters[0])) {
						HashMap<String, Object> values = new HashMap<String, Object>();

						for (String prop : inject.value()) {
							values.put(prop, getUnit().getModel().get(prop));
						}

						args.add(values);
					} else {
						args.add(getUnit().convert(getUnit().getModel().get(inject.value()[0]), parameters[0]));
					}
				} else {
					int i = 0;
					for (String prop : inject.value()) {
						args.add(getUnit().convert(getUnit().getModel().get(prop), parameters[i]));
						i++;
					}
				}

				return method.invoke(ref, args.toArray());
			}
			logger.warn("Action method (" + name + ") not called, any parameters found.");
			return null;
		}

		@Override
		public String toString() {
			return "action@" + name;
		}
	}
}
